const express = require("express");
const expressWs = require("express-ws");
const os = require("os");
const pty = require("node-pty");

const PLATFORM = os.platform();

const ENV = Object.assign({}, process.env);

const USE_BINARY = PLATFORM !== "win32";

const SHELL = PLATFORM === "win32" ? "powershell.exe" : "bash";

function startServer() {
	const app = express();
	const appWs = expressWs(app).app;

	appWs.ws("/terminal", function (ws, req) {
		const { query } = req;
		const { cols, rows } = query;
		if (typeof cols !== "string" || typeof rows !== "string") {
			console.error({ req });
			throw new Error("unexpected query args");
		}

		let term = pty.spawn(SHELL, [], {
			name: "xterm-256color",
			cols: parseInt(cols),
			rows: parseInt(rows),
			cwd: ENV.HOME,
			env: ENV,
			encoding: !USE_BINARY ? null : "utf8",
		});

		console.log(`terminal process ${term.pid} created`);

		// unbuffered delivery after user input
		let userInput = false;

		// string message buffering
		function buffer(socket, timeout, maxSize) {
			let s = "";
			let sender = null;
			return (data) => {
				s += data;
				if (s.length > maxSize || userInput) {
					userInput = false;
					socket.send(s);
					s = "";
					if (sender) {
						clearTimeout(sender);
						sender = null;
					}
				} else if (!sender) {
					sender = setTimeout(() => {
						socket.send(s);
						s = "";
						sender = null;
					}, timeout);
				}
			};
		}
		// binary message buffering
		function bufferUtf8(socket, timeout, maxSize) {
			const chunks = [];
			let length = 0;
			let sender = null;
			return (data) => {
				chunks.push(data);
				length += data.length;
				if (length > maxSize || userInput) {
					userInput = false;
					socket.send(Buffer.concat(chunks));
					chunks.length = 0;
					length = 0;
					if (sender) {
						clearTimeout(sender);
						sender = null;
					}
				} else if (!sender) {
					sender = setTimeout(() => {
						socket.send(Buffer.concat(chunks));
						chunks.length = 0;
						length = 0;
						sender = null;
					}, timeout);
				}
			};
		}

		const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 3, 262144);

		// WARNING: This is a naive implementation that will not throttle the flow of data. This means
		// it could flood the communication channel and make the terminal unresponsive. Learn more about
		// the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/
		term.onData(function (data) {
			// console.log("【 node pty output 】", data);
			try {
				send(data);
			} catch (ex) {
				// The webSocket is not open, ignore
			}
		});
		ws.on("message", function (msg) {
			// console.log("【 websocket get command 】", msg);
			try {
				term.write(msg);
			} catch (ex) {
				// The terminal is not exists, ignore
			}
			userInput = true;
		});
		ws.on("error", function () {
			console.log("websocket error");
		});
		ws.on("close", function () {
			process.kill(term.pid);
			console.log(`terminal process ${term.pid} closed`);
			term = null;
		});
	});
	app.use((err, req, res, next) => {
		console.error('express app error');
	});
	const port = parseInt(ENV.PORT ?? "9527");

	app.listen(port, () => {
		console.log(`node pty running on port ${port}`);
	});
}

startServer();
